﻿
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Input;
using System.IO;
using Kokomo.Environment.Commanding;
using Kokomo.Environment.Services;
using System.ComponentModel.Composition;

namespace Kokomo.Environment
{
	/// <summary>
	/// Represents what the user perceives as a document within the UI.
	/// </summary>
	/// <remarks>
	/// This class combines the data that defines a document with a
	/// file name, file type, activation state, dirty state, and
	/// commands to file and view operations.  With the exception of
	/// <see cref="ErrorDocumentViewModel"/>,
	/// the environment delegates creation to <see cref="DocumentEditor"/>s,
	/// and manages them with <see cref="IDocumentManager"/>.
	/// An instance may or may not be associated with a file,
	/// as determined by <see cref="FileName"/>.  Changing <see cref="FileName"/>
	/// also changes the <see cref="Title"/>.
	/// <para>
	/// Derived types must override <see cref="OnSaving"/> and set <see cref="CanSave"/>
	/// to <c>true</c> to support saving.
	/// Use <see cref="ViewTypeAttribute"/> to specify the UI for displaying the document.
	/// </para>
	/// </remarks>
	/// <seealso cref="DocumentEditor"/>
	/// <seealso cref="IDocumentManager"/>
	public class DocumentViewModel : RegionViewModel
	{
		/// <summary>
		/// Initializes a new instance of <see cref="DocumentViewModel"/>.
		/// </summary>
		protected DocumentViewModel()
		{
			this.CloseCommand = new DelegatedCommand(this.Close);
			this.FloatCommand = new DelegatedCommand(this.Float);
			this.RegisterCommand(CloseCommand, CloseCommandName);
		}

		private IEnumerable<MenuItemViewModel> _editorMenuItemViewModels;
		/// <summary>
		/// Gets a list of menu items for the Edit menu group.
		/// </summary>
		public IEnumerable<MenuItemViewModel> EditorMenuItemViewModels
		{
			get { return this._editorMenuItemViewModels; }
			set
			{
				this._editorMenuItemViewModels = value;
				this.OnPropertyChanged("EditorMenuItemViewModels");
			}
		}
		private IEnumerable<MenuItemViewModel> _objectMenuItemViewModels;
		/// <summary>
		/// Gets a list of menu items for the Object menu group.
		/// </summary>
		public IEnumerable<MenuItemViewModel> ObjectMenuItemViewModels
		{
			get { return this._objectMenuItemViewModels; }
			set
			{
				this._objectMenuItemViewModels = value;
				this.OnPropertyChanged("ObjectMenuItemViewModels");
			}
		}
		private IEnumerable<MenuItemViewModel> _helpMenuItemViewModels;
		/// <summary>
		/// Gets a list of menu items for the Help menu group.
		/// </summary>
		public IEnumerable<MenuItemViewModel> HelpMenuItemViewModels
		{
			get { return this._helpMenuItemViewModels; }
			set
			{
				this._helpMenuItemViewModels = value;
				this.OnPropertyChanged("HelpMenuItemViewModels");
			}
		}

		/// <summary>
		/// The name of untitled documents.
		/// </summary>
		public static readonly UiString UntitledDocumentTitle = UiString.FromString(Messages.UntitledDocumentTitle);

		private UiString _title;
		/// <summary>
		/// Gets or sets the title of the document.
		/// </summary>
		/// <remarks>
		/// The environment displays the document title on the tab
		/// corresponding to the document.
		/// If the document has no title, this property returns <c>Untitled</c>.
		/// Changing the <see cref="FileName"/> also changes the title.
		/// </remarks>
		/// <seealso cref="FileName"/>
		public UiString Title
		{
			get { return this._title ?? UntitledDocumentTitle; }
			set
			{
				this._title = value;
				this.OnTitleChanged();
			}
		}

		/// <summary>
		/// Raised when <see cref="Title"/> has changed.
		/// </summary>
		public event EventHandler TitleChanged;
		/// <summary>
		/// Raises the <see cref="TitleChanged"/> event.
		/// </summary>
		protected virtual void OnTitleChanged()
		{
			if (this.TitleChanged != null) this.TitleChanged(this, EventArgs.Empty);
			this.OnPropertyChanged("Title");
		}

		private bool _isDirty;
		/// <summary>
		/// Gets a value indicating whether the document contains unsaved changes.
		/// </summary>
		/// <remarks>
		/// This property is affected by <see cref="SaveTo"/>.
		/// </remarks>
		public bool IsDirty
		{
			get { return this._isDirty; }
			set
			{
				this._isDirty = value;
				this.OnIsDirtyChanged();
			}
		}
		/// <summary>
		/// Raised when <see cref="IsDirty"/> has changed.
		/// </summary>
		public event EventHandler IsDirtyChanged;
		/// <summary>
		/// Raises the <see cref="IsDirtyChanged"/> event.
		/// </summary>
		protected virtual void OnIsDirtyChanged()
		{
			if (this.IsDirtyChanged != null) this.IsDirtyChanged(this, EventArgs.Empty);
			this.OnPropertyChanged("IsDirty");
		}

		private bool _canSave;
		/// <summary>
		/// Gets a value indicating whether the document can be saved.
		/// </summary>
		/// <remarks>
		/// This property determines whether the environment allows the user
		/// to save the document.
		/// </remarks>
		public bool CanSave
		{
			get { return this._canSave; }
			protected set
			{
				this._canSave = value;
				this.OnCanSaveChanged();
			}
		}

		/// <summary>
		/// Raised when <see cref="CanSave"/> has changed.
		/// </summary>
		public event EventHandler CanSaveChanged;
		/// <summary>
		/// Raises the <see cref="CanSaveChanged"/> event.
		/// </summary>
		protected virtual void OnCanSaveChanged()
		{
			if (this.CanSaveChanged != null) this.CanSaveChanged(this, EventArgs.Empty);
			this.OnPropertyChanged("CanSave");
		}

		/// <summary>
		/// Gets a list of <see cref="FileType"/>s that the document can be saved as.
		/// </summary>
		/// <remarks>
		/// This property determines what file types the user can
		/// choose from when saving the document.
		/// </remarks>
		public FileType[] PossibleFileTypes { get; protected set; }

		private bool _isActive;
		/// <summary>
		/// Gets a value indicating whether the user is currently
		/// interacting with the document.
		/// </summary>
		public bool IsActive
		{
			get { return this._isActive; }
			set
			{
				this._isActive = value;
				this.OnIsActiveChanged();
			}
		}
		/// <summary>
		/// Raised when <see cref="IsActive"/> has changed.
		/// </summary>
		public event EventHandler IsActiveChanged;
		/// <summary>
		/// Raises the <see cref="IsActiveChanged"/> event.
		/// </summary>
		protected virtual void OnIsActiveChanged()
		{
			if (this.IsActiveChanged != null) this.IsActiveChanged(this, EventArgs.Empty);
			this.OnPropertyChanged("IsActive");
		}

		private string _fileName;
		/// <summary>
		/// Gets the name of the file storing the document.
		/// </summary>
		/// <remarks>
		/// Changing the file name updates the <see cref="Title"/>.
		/// </remarks>
		public string FileName
		{
			get { return this._fileName; }
			set
			{
				this._fileName = value;
				if (value == null)
				{
					//this.Title = null;
				}
				else
				{
					this.Title = UiString.FromString(Path.GetFileName(value));
				}
			}
		}

		#region Commands
		/// <summary>
		/// Gets the name of the <c>Close</c> command.
		/// </summary>
		public static readonly string CloseCommandName = "CloseDocument";
		/// <summary>
		/// Gets the <c>Close</c> command.
		/// </summary>
		public ICommand CloseCommand { get; private set; }
		/// <summary>
		/// Closes the document.
		/// </summary>
		public void Close()
		{
			Application.Current.Dispatcher.Invoke(
				(Action)this.OnClosing
				);
		}

		/// <summary>
		/// Raised when the document is closing.
		/// </summary>
		/// <seealso cref="Close"/>
		public event EventHandler Closing;
		/// <summary>
		/// Raises the <see cref="Closing"/> event.
		/// </summary>
		protected virtual void OnClosing()
		{
			if (this.Closing != null) this.Closing(this, EventArgs.Empty);
			if (this.MultiViewGroup != null)
				this.MultiViewGroup.ViewModels.Remove(this);
		}

		private bool _isFloating;
		/// <summary>
		/// Gets a value indicating whether the document is in a floating window.
		/// </summary>
		public bool IsFloating
		{
			get { return this._isFloating; }
			set
			{
				this._isFloating = value;
				this.OnPropertyChanged("IsFloating");
			}
		}
		/// <summary>
		/// Gets the <c>Float</c> command.
		/// </summary>
		public ICommand FloatCommand { get; private set; }
		/// <summary>
		/// Floats the document.
		/// </summary>
		/// <seealso cref="IsFloating"/>
		private void Float()
		{
			this.IsFloating = true;
		}
		#endregion

		/// <summary>
		/// Saves the document to a file.
		/// </summary>
		/// <param name="fileName">The name of the file to save to.</param>
		/// <param name="isPrimary">Indicates whether <paramref name="fileName"/>
		/// is the primary document file.</param>
		/// <exception cref="NotSupportedException">The document does not support saving.</exception>
		/// <remarks>
		/// When <paramref name="isPrimary"/> is <c>true</c>, this
		/// method updates <see cref="FileName"/> and sets <see cref="IsDirty"/>
		/// to <c>false</c>.
		/// Derived classes must override <see cref="OnSaving"/> to
		/// implement saving functionality.
		/// <para>
		/// Not all documents can be saved.  Check <see cref="CanSave"/>
		/// before calling this method.
		/// </para>
		/// </remarks>
		/// <seealso cref="OnSaving"/>
		/// <seealso cref="FileName"/>
		/// <seealso cref="IsDirty"/>
		/// <seealso cref="CanSave"/>
		public void SaveTo(string fileName, bool isPrimary)
		{
			if (fileName == null) throw new ArgumentNullException("fileName");

			this.OnSaving(fileName);
			if (isPrimary)
			{
				this.FileName = fileName;
				this.IsDirty = false;
			}
		}
		/// <summary>
		/// Called when the document is to be written to a file.
		/// </summary>
		/// <param name="fileName">The name of the file to write to.</param>
		/// <remarks>
		/// This method is called by <see cref="SaveTo"/>.
		/// The default implementation throws <see cref="NotSupportedException"/>.
		/// </remarks>
		protected virtual void OnSaving(string fileName)
		{
			throw new NotSupportedException();
		}

		#region Multi-view group
		private DocumentMultiViewGroup _multiViewGroup;
		internal DocumentMultiViewGroup MultiViewGroup
		{
			get { return this._multiViewGroup; }
			set
			{
				if (this._multiViewGroup != null)
				{
					this._multiViewGroup.Changed -= new EventHandler(MultiViewGroup_Changed);
				}
				this._multiViewGroup = value;
				if (value != null)
				{
					value.Changed += new EventHandler(MultiViewGroup_Changed);
				}
				this.OnMultiViewGroupChanged();
			}
		}

		void OnMultiViewGroupChanged()
		{
			this.OnPropertyChanged("HasMultipleViews");
			this.OnPropertyChanged("MultiViewIndex");
		}

		void MultiViewGroup_Changed(object sender, EventArgs e)
		{
			this.OnMultiViewGroupChanged();
		}

		/// <summary>
		/// Gets a value indicating whether this object supports multiple views.
		/// </summary>
		/// <remarks>
		/// The default implementation returns <c>false</c>.
		/// Derived classes that support multiple views must override this
		/// property to return <c>true</c> as well as override
		/// <see cref="CreateNewView"/>.
		/// </remarks>
		/// <seealso cref="CreateNewView"/>
		public virtual bool CanShowMultipleViews
		{
			get { return false; }
		}

		/// <summary>
		/// Gets a value indicating whether there are multiple views open.
		/// </summary>
		public bool HasMultipleViews
		{
			get { return this.MultiViewGroup != null && this.MultiViewGroup.ViewModels.Count > 1; }
		}

		/// <summary>
		/// Gets the one-based index of this instance within a multi-view group.
		/// </summary>
		/// <remarks>
		/// If this instance does not have multiple views,
		/// this property returns <c>0</c>.
		/// </remarks>
		public int MultiViewIndex
		{
			get { return (this.MultiViewGroup != null) ? this.MultiViewGroup.ViewModels.IndexOf(this) + 1 : 0; }
		}

		/// <summary>
		/// Creates a new view.
		/// </summary>
		/// <returns>The new <see cref="DocumentViewModel"/>.</returns>
		/// <remarks>
		/// The default implementation returns <c>null</c>.
		/// Derived classes that support multiple views must override this method
		/// as well as the <see cref="CanShowMultipleViews"/> property.
		/// Implementations must only create the view, not show it.
		/// </remarks>
		/// <seealso cref="CanShowMultipleViews"/>
		protected virtual DocumentViewModel CreateNewView()
		{
			return null;
		}

		/// <summary>
		/// Adds a <see cref="DocumentViewModel"/> to the current multi-view group.
		/// </summary>
		/// <param name="viewModel">The instance to add to the group.</param>
		/// <remarks>
		/// This implementation creates the <see cref="MultiViewGroup"/> if
		/// necessary but does not show <paramref name="viewModel"/>.
		/// </remarks>
		protected void AddViewToGroup(DocumentViewModel viewModel)
		{
			if (viewModel == null) throw new ArgumentNullException("viewModel");

			if (this.MultiViewGroup == null)
			{
				this.MultiViewGroup = new DocumentMultiViewGroup();
				this.MultiViewGroup.ViewModels.Add(this);
			}
			this.MultiViewGroup.ViewModels.Add(viewModel);
		}

		/// <summary>
		/// Opens a new view of this instance.
		/// </summary>
		/// <remarks>
		/// This method calls <see cref="CreateNewView"/> to create the new view,
		/// then calls <see cref="IDocumentManager.ShowDocument"/> to show it.
		/// </remarks>
		public void OpenNewView()
		{
			DocumentViewModel viewModel = this.CreateNewView();
			if (viewModel != null)
			{
				this.AddViewToGroup(viewModel);
				viewModel.MultiViewGroup = this.MultiViewGroup;
				EnvironmentServices.DemandService<IDocumentManager>().ShowDocument(viewModel);
			}
		}
		#endregion
	}
}
